feat(refit): add ModelExpress Megatron-to-vLLM reshard path - #3632
Open
KavinKrishnan wants to merge 25 commits into
Open
feat(refit): add ModelExpress Megatron-to-vLLM reshard path#3632KavinKrishnan wants to merge 25 commits into
KavinKrishnan wants to merge 25 commits into
Conversation
KavinKrishnan
force-pushed
the
kavink/upstream-mx-megatron-publisher
branch
from
August 13, 2026 21:32
feacdf7 to
537fa3a
Compare
KavinKrishnan
force-pushed
the
kavink/upstream-mx-megatron-publisher
branch
from
August 18, 2026 19:15
9c52bb6 to
6a49789
Compare
KavinKrishnan
marked this pull request as ready for review
August 18, 2026 20:07
Add the pure Megatron role and shard-geometry helpers needed to describe TP, ETP, and grouped-expert ownership without gathering model weights. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Map native Megatron names and shard geometry to ModelExpress HF aliases, including global expert IDs and fail-closed fused gate/up ordering. Cover the full Qwen3-30B EP8 name set in CPU-only tests. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Remove obsolete v2 and EAGLE-specific material from the extracted helper, document the current ModelExpress seam, and skip integration-only tests when the optional dependency is absent. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Wire ModelExpress's reshard refit path into GRPO as a `refit_transport`
option alongside `nccl_reshard`. The trainer publishes a Megatron reshard
view with HF-named aliases; each vLLM receiver then pulls exactly the
slices its own TP rank needs over NIXL/UCX RDMA, rather than having the
trainer gather and broadcast full tensors.
Publish and receive are strictly sequential and both sit on the critical
path, so `sync_weights` times them separately as
`prepare_for_generation/mx_reshard_{publish,receive}`. Only the receive
half emits MX_REFIT_STAGE records, so without the split the publish cost
is invisible; on Qwen3-30B-A3B it is the larger of the two by roughly 3x.
Version ordering is deliberate: every trainer publishes version N before
any receiver begins pulling it, so a receiver can never observe a fleet
where some trainers still advertise N-1.
Validated on 32 GPUs (GB200, RoCE) against Qwen3-30B-A3B-Instruct
(TP2/EP4/ETP1 trainer, four TP4 vLLM replicas) and Qwen3-4B: 100%
coverage, 0 fallback, 11 consecutive refits, and `gen_kl_error` matching
the pre-existing transport on the dense model.
Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Move the training call into try/finally so cluster and generation shutdown run even when training raises. Previously an exception skipped teardown, leaving NIXL agents with registered memory and Ray actors alive; the process then aborted during interpreter shutdown and buried the original traceback under an unrelated fatal error. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
The functional test gates on `max(train/token_mult_prob_error) < 1.05`,
matching the existing nccl_reshard test, so a refit that installs wrong
or stale weights fails rather than merely running.
The bench configs cover the GB200/RoCE topologies used to validate this
transport. Two environment settings there are load-bearing and easy to
lose:
- UCX_TLS excludes `ud` as well as `tcp`. With a live NCCL process group
in the same process, destroying a NIXL agent aborts in ud_iface.c
("unable to remove iface timer handler"); rdmacm handles wireup, so
dropping UD costs nothing measurable.
- NCCL_CROSS_NIC=0. Each node holds an address on all four RDMA rails,
and NCCL will otherwise pair a local rail-0 HCA against a peer's
rail-3 address. Those are different subnets, so RoCE never connects and
the queue pair dies with IBV_WC_RETRY_EXC_ERR.
`megatron_cfg.checkpoint.async_save` is off: these recipes never write a
checkpoint, and the persistent async-checkpoint worker's
multiprocessing.Manager fork intermittently dies during worker init,
killing the run with an EOFError. It cost 2 of the first 5 MoE runs.
Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
The refit's dominant cost was invisible. MX_REFIT_STAGE records cover only the install, so MX telemetry reported 1.9 s of a 10.25 s refit and looked healthy. Emitting discover_s alongside mx_update_s shows the quorum check is 5.20 s, or 51% of the refit and 2.7x the actual weight transfer. It re-fetches and re-parses 78760 tensor entries from 16 trainer sources on every refit: one list_sources plus a get_metadata round-trip per rank, each returning that rank's whole shard table. None of it changes between steps; only publisher_step does. The cost scales with source count rather than bytes, which is why a 30B MoE with 18432 expert tensors exposed it and a dense 4B (~6400 entries, ~0.4 s) did not. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
The phase split read payload.tensors directly, so any rendezvous payload without a shard table raised AttributeError *after* the weights had already installed, turning a successful refit into a failed one. Reporting must not be able to break the operation it measures. Missing shard tables now count as zero and the emit is guarded, with regression tests for both the absent-field case and the counting itself. Caught by the unit suite, which had never been run against these changes. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
The receiver's per-step version check does not read shard geometry; MX discovers that once in _prepare and keeps it. Requesting the tables anyway rebuilt 78,760 entries across 16 ranks on every refit. Falls back to the full fetch against an MX predating the flag, so the two changes need not land together, and the entry count now comes from the payload's own tally so the metric that exposed this cost does not read zero once the tables are skipped. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
gen_kl_error is a k3 estimator, and the benchmark recipe feeds it at most 128 generated tokens per step, where one outlier token moves it a long way. This raises max_new_tokens 8 -> 128 and changes nothing else, so batch sizes and the refit path are untouched. The result was negative and worth keeping: the estimate did not fall below the 1e-3 guideline, it stabilized at ~1.5e-3 with the spread narrowing from 5.5x to 2.6x. So the exceedance is real rather than sampling noise, and the no-refit reference is still needed to attribute it. See doc 21. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Every other refit transport can check the weights it moved: SGLang has check_weights(compare), the sparse transports emit delta_verify/*. mx_reshard had only end-to-end logprob metrics, which conflate refit fidelity with Megatron-vs-vLLM implementation divergence. MX_REFIT_VERIFY=1 fingerprints every vLLM parameter before and after each install and emits MX_REFIT_VERIFY naming what changed. Fingerprints are two allocation-free int64 reductions over the raw bytes, so they catch a single flipped mantissa bit, where a float statistic can miss one; retaining a pre-refit copy would instead cost ~15 GB per rank. Off by default, since it sits on the refit critical path, and it cannot fail the refit it verifies. Running it found something that matters more than the feature: the benchmark recipes never train. max_new_tokens=8 means no generation can be correct, so every reward is 0, the leave-one-out baseline makes every advantage 0, and Loss is 0.0000 at every step. Confirmed against lr=1e-2, where nothing changed either. So the first refit changes all 435 params and every later refit transports byte-identical weights. Performance rows are unaffected, but from step 2 on the correctness gates cannot tell a working refit from a no-op, because the correct answer is "no change". See doc 22. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
grpo_math_1B_megatron.yaml has no refit_cfg subtree, so Hydra refused the plain `=` overrides for refit_transport and mx_reshard.server_url and the gate died during config parsing, before training. Switch both to `++`. Earlier validation replayed archived TensorBoard data through check_metrics.py, which exercised the assertion but never the launch. Running the gate live on 2 GPUs now reaches the end and passes at token_mult_prob_error 1.0154 < 1.05. Also adds topoA_correctness.yaml, a correctness arm that generates 512 tokens with 4 generations per prompt so rewards can differ within a group. The performance recipes generate 8 tokens, score every sample 0, and never train, which made every post-first refit gate vacuous. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
The ratio check alone passes on a run that never trains. GRPO's leave-one-out baseline zeroes the advantage of any prompt group whose rewards are all equal, so if no group is mixed the second refit re-sends bit-identical weights and `token_mult_prob_error < 1.05` is satisfied by a no-op (doc 22). Assert `max(train/grad_norm) > 0` so the ratio check is only credited on a model that moved, and widen the reward sample from 2x4 to 4x8 so a zero-gradient run is rare rather than a coin flip. The previous shape solved 1 of 8 sequences, and that single sequence produced the entire gradient. Verified against the archived metrics of a live gate run: the assertion passes at grad_norm 5.21, and a negative control with grad_norm zeroed fails the new check while still passing the ratio check, which is the vacuity it is meant to catch. The widened batch shape itself is not yet confirmed on a live run; cluster capacity was reclaimed mid-validation. Also adds a single-node smoke config that runs the gate twice, once BF16 and once with a vLLM MXFP8 rollout, to exercise FP8 on this path for the first time. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
…lure Two things learned by running it. vLLM FP8 generation asserts use_importance_sampling_correction in grpo.py, so without that override the run dies in setup before any refit. With it, the arm reaches the first refit, the transport completes cleanly (898 descriptors, 0 full-pull sources), and the install raises "Cannot copy out of meta tensor" inside ModelExpress's quantized commit path. That is an MX-side bug, so the arm is kept as its reproducer rather than removed. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Four steps proved weights move and the gate holds while they do, but cannot say whether per-refit error accumulates, whether gen_kl_error keeps creeping past ~1.4e-3, or whether discover_s stays flat across many refits. Same 512-token, 4x4 shape as the 4-step arm, twelve steps. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
gen_kl_error mixes runtime divergence with refit error, and only the sum was ever measured. This arm separates them by having vLLM hold the real checkpoint before the first refit, so both sides provably hold identical weights. Result: the first refit changes 0 of 435 parameters on all 16 receiver ranks, so the refit is bit-exact and gen_kl_error at that point is pure Megatron-vs-vLLM divergence: 8.7e-4 and 1.3e-3. The documented < 1e-3 gate therefore sits below the model's own floor, and no refit can pass it. See doc 27. The arm needs a diagnostic escape hatch for load_format, kept out of this branch and archived alongside the evidence, because NeMo-RL forces load_format=dummy whenever a refit transport is set. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
The CI gate had no KL-family assertion, and the obvious candidate, an absolute gen_kl_error < 1e-3, is not safe to add: that metric has a non-zero floor set by Megatron-vs-vLLM kernel differences which grows with model size. Measured with a refit that provably changed no parameter, the floor is 8.7e-4 to 1.3e-3 on Qwen3-30B-A3B, so the bound is below the floor and unpassable there regardless of refit correctness. js_divergence_error is bounded and symmetric, and a single 1e-3 bound holds across every scale measured: 1.6e-4 on 0.6B, 1.3e-4 on 4B dense, 5.0e-4 on 30B MoE. Validated offline against both archived gate runs (pass at 1.5e-4 and 1.6e-4) and against a negative control with the metric inflated to 1.1e-3 (fails). See doc 27. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
summarize_refit_stages.py answers how fast a run was; it cannot say why two identical runs differ. This adds the other cut: per-stage spread across runs, and whether the slowest rank is the same one each time, which separates placement from contention. It deliberately reports stages for the rank that was fleet-critical on each step rather than the median across ranks, because those disagree. On the dense arm the all-rank median of wire_fused_s moves 1.90x between two cold starts while the fleet-critical total moves only 1.03x: the body of the distribution shifts but the tail sets the refit duration. Only the critical rank is on the critical path, so attributing from all-rank medians would point at a stage that is not the problem. topoA_variance.yaml runs the MoE arm at the full 11 steps for doc 19 item 8. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Resolve QKV heads from each live Megatron layer so heterogeneous attention and KV-heads-below-TP layouts reach ModelExpress without zero local KV counts. Keep the root config only as a validated compatibility fallback. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Retain only portable recipes and analysis tools; namespace- and private-image- specific validation manifests remain internal evidence rather than upstream API. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Keep Megatron generation on its native synchronizer while ensuring non-colocated MX and NCCL reshard paths skip the legacy HF refit-info handshake. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
The PR description promises portable recipes with no private cluster detail, but three headers still carried a colleague's name, an internal design-doc filename, internal cluster state, and a stale claim that fused grouped-expert install is unimplemented. That claim is no longer true once ModelExpress NVIDIA-NeMo#635 lands its fused MoE capture fix, so a reader would have been told the dense recipe works around a gap that no longer exists. Keep the technical rationale each header carried -- geometry, why a dense arm isolates non-expert resharding, why the KL sample recipe raises max_new_tokens -- and drop the parts that only mean something inside the originating cluster. Also replace the "NVIDIA-NeMo#496" ModelExpress PR reference in canonicalize_grouped_expert_name with a description of the behaviour it depends on, since that number resolves to nothing for a NeMo-RL reader. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
The repo runs ruff and ruff-format in pre-commit, and these files were left unformatted, so the hook would have failed on them. Reflow only: the changes are line breaks, parentheses and trailing commas, with no token otherwise altered. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
The quorum check asks MX for `with_tensors=False` to skip rebuilding a shard table that is identical every step, and fell back to the full fetch when MX predated the flag by catching TypeError. That also caught a TypeError raised *inside* discovery, silently retrying it as the fetch it was trying to avoid, so a genuine bug in MX would have surfaced as an unexplained per-step slowdown rather than as an error. Ask the signature instead, which is what the publish path already does for `publish_registered_shard_table`, and cache the answer: the installed MX cannot change under a live receiver. The tests could not express this before, because a bare MagicMock reports only *args/**kwargs and so matches neither contract. Autospec two stubs carrying the real signatures, which additionally makes a call the installed MX would reject fail in the test too, and cover the swallowed-TypeError regression directly. Also drop the "NVIDIA-NeMo#635" references from this module: the PR number resolves to nothing for a NeMo-RL reader, and the error message now names what the adapter needs and where to look instead. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Five findings from a pass over the reshard sources.
`_bridge_module_type_registry` re-ran the Bridge import and copied the registry
once per parameter while classifying a publish set, which is thousands of times
per refit for a 30B model, to read a value that cannot change mid-process. Cache
it, and freeze the sets so a caller cannot mutate the cached copy.
`publish_megatron_hf_aliases` built every alias before rejecting a negative
`publisher_step`. Validate the argument first.
`published_byte_count` was exported and tested but never called, and
`MxMegatronPublisher.publish` discarded the table that
`publish_megatron_hf_aliases` documents itself as returning so a caller can
report counts without rebuilding it. Wire them together into a publish-side
phase record, mirroring the receiver's MX_RECV_PHASE: the synchronizer already
times this half, but elapsed time alone cannot distinguish a publish that
described more shards from one that described the same shards more slowly.
Reporting is guarded, because the bytes are already out by then.
`fingerprint_model` returns {} when a parameter cannot be fingerprinted, which
disables verification for that refit. It did so silently, so a verification tool
that had switched itself off reported the same thing as one that was passing.
Say so instead.
Finally, two comments that did not match their code: `UnmappedMegatronTensor`
promises the publish fails rather than skipping a tensor, while
`build_megatron_alias_inputs` does skip one -- reachable only through a
non-strict resolver, which is now stated -- and the version counter's
last-committed semantics are safe only because a failed refit aborts the run,
which anything adding a retry needs to know.
Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
KavinKrishnan
force-pushed
the
kavink/upstream-mx-megatron-publisher
branch
from
August 19, 2026 19:40
e45786a to
b28a2fd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
This PR adds the complete NeMo-RL side of the ModelExpress
mx_reshardrefit path:Each part has a narrow job in making trainer-to-generator refits usable from NeMo-RL:
WeightSynchronizernow starts publishers and receivers, waits for all required ranks, runs the transfer, commits the new version, and shuts both sides down in a safe order. Callers do not need a separate ModelExpress-only control path.linear_qkvmodule avoids2 // 8 = 0and prevents one root-model setting from being incorrectly reused for every layer.ModelExpress dependency
This path needs ModelExpress
mainat or afterf4660d2. ModelExpress #635 carried the transport work and the global QKV interval alias builder this PR's QKV contract requires, and it merged on 2026-08-19, so no ModelExpress branch is needed any more. It was squash-merged, so pin by commit rather than by ancestry.Neither half is useful alone: NeMo-RL publishes the geometry, ModelExpress maps the intervals. ModelExpress #482 does not work — it lacks
VllmReshardReceiver(modelexpress.engines.vllm.refit.receiver), which is what this PR imports.Enable the path with
policy.generation.refit_transport: mx_reshard. This is not the legacycluster.weight_sync.method: mxpath; the two are unrelated.Architecture
flowchart LR M[Live Megatron layer] --> G[Per-layer Q/KV geometry resolver] G --> C[Parameter classifier] C --> B[Megatron-Bridge name map] B --> P[ModelExpress publisher] P --> Q[Publisher quorum] Q --> R[vLLM reshard receiver] R --> I[Install and verify] I --> V[Commit one version] W[WeightSynchronizer] --> P W --> Q W --> R W --> V W --> S[Ordered receiver/publisher shutdown]For Q=64, KV=2 and trainer TP8, NeMo-RL now publishes global
num_heads,num_kv_heads,head_dim, andqkv_interleavefrom each livelinear_qkv.config. It does not publishnum_kv_heads_local=0, and it does not pretend every rank owns a replicated KV head. ModelExpress receives each rank's real fused-row range and performs the interval mapping.Why per-layer geometry matters
Nemotron-family models may use different attention geometry in different layers. Reading one root model config would stamp the same Q/KV shape onto every QKV tensor. The resolver therefore walks to the owning live
linear_qkvmodule first and uses the root values only as a compatibility fallback. The chosen geometry must match the tensor's global fused-row count or publication fails closed.Performance measured on real model tensors
These are three different experiments, not interchangeable rows from one run:
The dense and MoE runs use 8 nodes x 4 NVIDIA GB200 = 32 GPUs, split into 16 non-colocated trainer GPUs and 16 generation GPUs. Nodes are
arm64; each has four 189 GB GPUs and four RDMA rails. Weight refit uses NIXL/UCX RDMA. NCCL is used only for Megatron-internal collectives, not as the refit transport. All three arms use BF16.For performance rows, the methodology is three independent cold starts per arm, then one warm-up plus ten measured warm refits. "Fleet-critical receiver latency" means: for each refit, take the slowest receiver rank because the fleet cannot continue until that rank finishes; then report the distribution of those per-refit maxima. It is not an average-rank latency.
An attribution note, so these numbers are read correctly: the byte and latency reductions below come from the ModelExpress transport changes (replica deduplication, descriptor caching, batched install, fused expert capture). This PR is the NeMo-RL half that makes that transport reachable from GRPO, enforces the version/quorum ordering it needs, and supplies the telemetry that produced every figure here. The rows are what a NeMo-RL user actually gets end to end, not a claim that this diff alone caused them.
Dense Qwen3-4B: replica-deduplication A/B
This table belongs only to the dense setup above: Qwen3-4B, Megatron TP2/DP8 on 16 GPUs -> four vLLM TP4 replicas on 16 GPUs. Same model checkpoint, prompts, NeMo-RL tree, nodes and placement in both arms; only ModelExpress differs (
9abaf2dd, the transport's parent, versus3ddb9b16, the six-change candidate).9abaf2dd3ddb9b16transfer_and_update_weights, medianComparing median-of-three cold starts against median-of-three, the receiver speedup is 7.57x (4.526 s -> 0.598 s). Byte counts were bit-identical across all three cold starts in each arm, with
fallbackandfull_pull_sourcesat 0 throughout.Please read the throughput row carefully and do not quote it as a regression. Per-rank throughput goes down while wall clock improves 7.6x. The baseline sustains a higher rate precisely because it moves 8x more data with eight concurrent sources per shard, which amortises per-descriptor overhead well; the new path finishes so much sooner that fixed costs occupy a larger share of a much shorter transfer. The win is not moving redundant bytes at all — the trainer runs DP8, so every weight exists on eight data-parallel replicas and the receiver was re-reading the same bytes — rather than driving the fabric harder.
The framework-visible 5.84x is the honest user-facing number, and it is smaller than the receiver-side ratio because it also carries publisher-side preparation and Ray call overhead.
MoE Qwen3-30B-A3B: expert coverage
This table belongs only to the MoE setup above: Qwen3-30B-A3B, Megatron TP2/EP4/DP2 on 16 GPUs -> four vLLM TP4 replicas on 16 GPUs. "Before" is the same model and topology without fused grouped-expert capture; "after" is the current reshard path. Before the fix, MX deliberately failed closed at ~5% coverage rather than activating a partial or stale model. The 18,432 unsupported entries are the model's grouped expert tensors, not arbitrary transfer failures.
coverage_pctcopies_capturedunsupportedparams_installed/engine_paramsparams_never_writtendest_bytes/engine_bytesfallbackSustained across 11 consecutive refits, then re-confirmed over 12 refits, and re-confirmed again against merged ModelExpress main.
Per receiver rank per refit on the MoE arm:
planned_wire_bytes29,780,766,720 againstengine_bytes15,285,252,096, so 14,495,514,624 extra — a 1.948x amplification inherent to resharding TP2/EP4 into TP4, with 12,693,891 exact descriptors andfallback0.MoE Qwen3-30B-A3B: where one 10.25 s refit goes
This is a separate warm timing decomposition of the same Qwen3-30B-A3B topology, not the dense A/B above. It is the least flattering number and the most useful one for capacity planning. It is also only visible because of the stage split added in
b236bc35d; before that, ModelExpress telemetry covered the install alone and reported 1.9 s of a 10.25 s refit, which looked healthy.A warm MoE refit (Qwen3-30B-A3B) costs 10.25 s end to end:
mx_reshard_publishmx_reshard_receive->discover_trainersmx_reshard_receive->update_weights(transfer + install)Each phase is reported from its own median, so the rounded times and shares must not be added as if they were three spans from one trace; that is why the displayed shares total 102%. The interpretation is still stable: metadata discovery is the dominant term, publisher preparation is second, and transfer plus install is smallest.
The largest single cost in a refit is a metadata quorum check, at 2.7x the actual weight transfer. Refit is 45.6% of step time, so
discover_trainersalone is roughly 22% of every training step. It re-fetches and re-parses 78,760 tensor entries across 16 trainer sources on every refit when the only thing that changed ispublisher_step. The cost scales with source count rather than bytes, which is why a 30B MoE with 18,432 expert tensors exposed it and a dense 4B (~6,400 entries, ~0.4 s) did not.This is a known, unfixed ceiling that needs an upstream ModelExpress protocol change to carry
publisher_stepin thelist_sourcesrecord, making the check O(1) and model-size independent. Projected gain is ~10.3 s -> ~5.1 s per refit, about -22% step time.Client-side mitigations are exhausted and measured, so please do not ask for them without new information. Fetching the per-source metadata concurrently made things 49% worse (total discover cost 5.41 s -> 8.04 s, the fetch term alone 4.02 s -> 7.56 s): the client looks idle while waiting, but the resource it waits on is the shared metadata server, so concurrency only adds contention. Skipping the shard-table rebuild did what it claimed locally, 0.79 s -> 0.20 s of parse time, but produced no end-to-end gain because the fetch dominates.
eae031f60keeps the cheap version-stamp path, and the serial fetch is pinned by a test that a concurrent implementation fails.MoE Qwen3-30B-A3B: run-to-run variance
This is receiver-side transfer/install timing for the same MoE topology across three independently created clusters. It is not the 10.25 s framework refit total above: it isolates the receiver critical path so the source of run-to-run spread can be attributed. Fleet-critical medians were 1.845 s, 1.424 s and 1.870 s: a 1.31x across-run spread.
attribute_variance.pylocalises it:wire_fused_sinstall_sAll of the variance is the wire; the receiver-side install is deterministic to four decimal places and has no headroom left. The straggler rank moves between runs, so this is fabric contention rather than bad placement. Practical consequence: measure any transport change with at least three cold starts, because a single run can be 1.31x off, which is larger than a plausible incremental win and can either hide a real improvement or manufacture a fake one.
What is not a performance claim
The Q=64/KV=2 TP8 -> TP8 arm described above is a correctness arm and its timings are meaningless: the model is deliberately reduced to four layers, and each TP8 group spans two 4-GPU nodes, so the transport crosses the fabric for a geometry no production run would choose. Its valid result is only: after vLLM loaded the same checkpoint, refit changed 0 of 34 parameters on all 8 receiver ranks. The same timing exclusion applies to the 12-step correctness arms, which use 512-token generation to make the gates non-vacuous rather than to benchmark latency.
Correctness gates
These are from the Qwen3-30B-A3B MoE topology, not from the dense timing arm or the reduced KV<TP model: Megatron TP2/EP4/DP2 on 16 GPUs -> four vLLM TP4 replicas on 16 GPUs. The correctness recipe runs 12 refits with 512 generated tokens and 4 prompts x 4 generations so rewards differ within a GRPO group and weights actually move. It is intentionally longer and more generation-heavy than the performance recipe.
Thresholds enforced by the functional gate, with what that 12-refit loop measured against merged ModelExpress main:
token_mult_prob_errorjs_divergence_errorgrad_normfallback/unsupportedTwo deliberate choices worth understanding before changing them:
js_divergence_error, not an absolutegen_kl_errorceiling. Megatron and vLLM kernels have a non-zero baseline KL even when every parameter is bit-identical; measured with a refit that provably changed no parameter, that floor is 8.7e-4 to 1.3e-3 on Qwen3-30B-A3B, so agen_kl_error < 1e-3gate sits below its own noise floor and would fail a perfect refit.js_divergence_erroris bounded and symmetric, and a single 1e-3 bound holds at every scale measured: 1.6e-4 on 0.6B, 1.3e-4 on 4B dense, 5.0e-4 on 30B MoE.Review guide
1. QKV/KV-heads-below-TP contract
Commit:
83c3d1324Review first:
nemo_rl/distributed/mx_megatron_helpers.pynemo_rl/models/policy/workers/megatron_policy_worker.pynemo_rl/weight_sync/mx_reshard_weight_synchronizer.pyPlease check that layer-local config wins, local head fields are emitted only when both counts divide by TP, fused rows are validated, and only the obsolete
num_query_groups % TPguard was relaxed.2. Megatron publisher foundation
Commits:
9bcc3b8ea,c966932a0,9446bbce4Review parameter role classification, global expert IDs, Megatron-Bridge name order, TP/ETP ranges, and fail-closed handling of unknown or ambiguous fused layouts.
3. Weight-sync and vLLM lifecycle
Commit:
84f13c2f7, with cleanup inaa2827a29Review:
mx_reshard_weight_synchronizer.pymx_reshard_publisher.pymx_vllm_reshard_receiver.pymx_reshard_config.pyThe important ordering is publish all trainer ranks, establish receiver quorum, pull/install on all receiver ranks, then commit. Any failed rank prevents version commit. Shutdown releases receivers before publishers so NIXL registrations remain valid until reads finish.
4. Reliability and observability
Commits:
b236bc35d,930f66a36,eae031f60,c473aef94These are the commits behind the performance section.
b236bc35dsplitsdiscover_sfrommx_update_sand is what exposed the quorum cost;eae031f60narrows the per-step check to the version stamp and falls back cleanly against an older ModelExpress, so the two repos need not land together;930f66a36makes the reporting unable to fail the refit it measures;c473aef94addsMX_REFIT_VERIFY.Review cleanup after exceptions, telemetry isolation, separate quorum timing, and parameter verification in
mx_refit_verify.py. Telemetry must never turn a successful refit into a failed one.5. Functional gate and portable tools
Review
tests/functional/grpo_mx_reshard_refit.sh, the portable recipe configs,summarize_refit_stages.py, andattribute_variance.py.attribute_variance.pydeliberately reports stages for the rank that was fleet-critical on each step rather than the median across ranks, because those disagree: on the dense arm the all-rank median ofwire_fused_smoves 1.90x between two cold starts while the fleet-critical total moves only 1.03x. Only the critical rank is on the critical path, so attributing from all-rank medians points at a stage that is not the problem.Private namespace/image-specific cluster manifests are intentionally not included.
Evidence and scope
Validated at 32 GPUs (16 Megatron trainer GPUs, 16 vLLM receiver GPUs) on BF16:
KV heads below TP is now proven end to end, not only unit-qualified. A reduced model with Q=64, KV=2, head_dim=128 at Megatron TP8 -> vLLM TP8 reproduced the source checkpoint bit for bit: 0 of 34 parameters changed on all 8 receiver ranks, with vLLM holding the real checkpoint before the first refit so the comparison is exact rather than statistical. Zero geometry errors, and the sparse publish pattern is visible in the per-rank tensor counts. The earlier unit and CUDA-tensor qualification (exact Q/K/V reconstruction, same-weight and changed-weight parity, projection-output equality) still holds underneath it.
This is still not full Nemotron Ultra qualification: the E2E arm is a reduced 4-layer model without EP64, so KV<TP combined with EP64 and the full Ultra architecture remain open.
Re-verified after ModelExpress #635 merged: the 12-refit MoE loop and the KV<TP exact-verify arm were both re-run against ModelExpress
mainatf4660d2, with every gate passing at values equal to or slightly better than the pre-merge results. That was not a formality — main also picked up four unrelated ModelExpress PRs and a refactor of the QKV alias builder itself.FP8 installation on this path is a known gap, not a silent one:
4bbaf6a54keeps the arm as a reproducer. The transport completes cleanly and the install then raises "Cannot copy out of meta tensor" inside ModelExpress's quantized commit path, which is a ModelExpress-side bug.Test plan
mx_reshardsynchronizer suite — 14 passedmx_reshardsuites re-run against merged ModelExpressmain— 78 passedmain— 503 passedmain(f4660d2)e45786afd